
今日目的:把
unit-test加進 Pipeline,並且讓它紅燈時你看得懂在講什麼。
先備知識:銜接 Day 21〈Trivy fs〉。
本篇所有數字、log、PipelineRun 狀態都是 2026-08-22 在cinamespace 實跑的。
環境:CRC 4.21.14 / Nx 23.1.1 / Jest 30.3 / 單一專案apps/web。
Jest 是 JavaScript/TypeScript 的測試框架。*.spec.ts 裡的 describe、it、expect 就是它的 API,nx test 底下實際在跑的也是它。
本文只用到它的三件事:跑測試、算覆蓋率、拿覆蓋率去比對門檻(沒過就 exit 1)。
覆蓋率報表的四個欄位,全篇一直出現:
if/三元運算子這種分岔,每一條路有沒有走過設定寫在 apps/web/jest.config.cts,Nx 則是透過 @nx/jest:jest executor 去呼叫它 —— 這個中間層等一下會製造麻煩(見 §10.2、§10.3)。
第一部(快樂路徑):unit-test 接在 npm-build 之後、跟 eslint-check 並行。Task 只有一個 step、五個旗標,jest.config.cts 只加三段。整個 Task 跑完 7~21 秒。
第二部(紅燈處理):紅燈的訊息長這樣 ——
Jest: Uncovered count for statements (46) exceeds global threshold (42)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
測試全過,Task 還是紅燈。 它擋的是「覆蓋率退步」,不是「測試壞掉」。修法是補測試 —— 而且要補夠:分支沒走到,照樣紅燈。
| 項目 | 加 unit-test 之前 |
之後 |
|---|---|---|
| DAG | npm-build → eslint-check |
npm-build → eslint-check ∥ unit-test |
| 測試檔全刪掉 | 沒有這道關卡 | exit 1,No tests found, exiting with code 1 |
| 覆蓋率報表 | 沒有 | 文字表格印在 Pod log |
| 帳面覆蓋率 | 100 / 100 / 100 / 100(假的,只算 3 個檔) | 22.22 / 0 / 16.66 / 16(9 個檔) |
| 新增沒測的程式碼 | 綠燈通過 | unit-test StepFailed |
| 整條 Pipeline 耗時 | — | 只多約 20 秒(跟 eslint-check 並行) |
unit-test 需要 node_modules,所以接在 npm-build 之後。它跟 eslint-check 沒有先後關係,Tekton 會讓兩者並行:
git-clone ──┬─► workspace-probe ─┐
├─► gitleaks-scan ───┤
├─► semgrep-scan ────┼─► npm-build ──┬─► eslint-check
└─► sca-scan ────────┘ └─► unit-test
- name: unit-test
runAfter:
- npm-build
params:
- name: NX_PROJECT
value: web
taskRef:
kind: Task
name: unit-test
workspaces:
- name: source
workspace: shared-workspace
npm-build的runAfter是四個 Task 的匯流點(gitleaks-scan、semgrep-scan、sca-scan、workspace-probe全部完成才開始),不是鬆散的扇出。四個掃描裡任何一個紅燈,npm-build和它後面的eslint-check、unit-test全部不會跑。
並行的好處:一次 run 就同時知道「風格/型別有沒有問題」和「覆蓋率有沒有退步」。實測 unit-test 只花 7~21 秒,整條線的 critical path 幾乎不變。
apiVersion: tekton.dev/v1
kind: Task
metadata:
name: unit-test
namespace: ci
spec:
params:
- name: NX_PROJECT
type: string
default: web
steps:
- name: test
image: nexus-nexus-proxy.apps-crc.testing/docker-proxy/library/node:22-alpine
workingDir: $(workspaces.source.path)
env:
- name: NODE_OPTIONS
value: --max-old-space-size=3072
- name: NX_DAEMON
value: "false"
- name: CI
value: "true"
script: |
#!/bin/sh
set -e
test -d node_modules || { echo "沒有 node_modules,npm-build 沒跑過"; exit 1; }
npx nx run-many --target=test \
--projects="$(params.NX_PROJECT)" \
--parallel=1 \
--coverage \
--coverageReporters=text \
--passWithNoTests=false \
--skip-nx-cache
workspaces:
- name: source
| 旗標 | 拿掉會怎樣 |
|---|---|
--coverage |
coverageThreshold 根本不會被檢查,門檻形同虛設 |
--coverageReporters=text |
覆蓋率有量,但報表只產成 HTML 留在容器裡,Pod log 一個數字都沒有(見 §10.2) |
--passWithNoTests=false |
測試檔全消失也是綠燈(見 §10.3) |
--skip-nx-cache |
nx 可能重播上一次的結果。被快取重播的綠燈,在 log 上跟真跑出來的綠燈長得一模一樣(見 §10.5) |
--parallel=1 |
跟 eslint-check 一致;單機不要讓多個 Node 行程搶記憶體(見 §12) |
npm cinode_modules 由 npm-build 裝在 shared workspace,unit-test 直接用。實測 npm ci 走 Nexus 要 31 秒 / 1726 個套件 / 774.9 MB,重跑一次是純浪費。
第一行 test -d node_modules 是防呆:有人把 runAfter 拿掉時,它會明確報錯,而不是丟出一個看起來像測試失敗的錯誤。
jest.config.cts 的三段Task 只是把旗標傳進去,真正決定「量什麼、怎麼報、擋在哪」的是設定檔。
apps/web/jest.config.cts(注意副檔名是 .cts,不是 .ts —— grep --include="*.ts" 掃不到它):
module.exports = {
displayName: 'web',
preset: '../../jest.preset.js',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
coverageDirectory: '../../coverage/apps/web',
// ① 報表出口
coverageReporters: ['text', 'json-summary', 'html'],
// ② 統計範圍
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.spec.ts',
'!src/**/*.d.ts',
'!src/test-setup.ts',
],
// ③ 門檻
coverageThreshold: {
global: {
statements: -42,
branches: -8,
functions: -5,
lines: -42,
},
},
// 以下維持 repo 原本的內容
transform: { /* ... */ },
};
@nx/jest 的 preset 把 coverageReporters 覆寫成 ['html'](Jest 原生預設是 ["json","lcov","text","clover"],含 text)。不覆寫的話報表只產成 HTML 留在容器裡,Pod 一結束就沒了。詳見 §10.2。
Jest 沒設 collectCoverageFrom 時,統計範圍是「測試執行過程中實際被 import 到的檔案」,不是整個專案的原始碼。
apps/web 有 1 個 spec、9 個原始碼檔。不設的話:
File | % Stmts | % Branch | % Funcs | % Lines |
---------------|---------|----------|---------|---------|
All files | 100 | 100 | 100 | 100 |
app.html | 100 | 100 | 100 | 100 |
app.ts | 100 | 100 | 100 | 100 |
nx-welcome.ts | 100 | 100 | 100 | 100 |
四項全部 100%,表格只有 3 行。 這個 100% 的真正意思是「被測到的那 3 個檔案測得不錯」。
補上 collectCoverageFrom 之後:
| 指標 | 不設 | 設了 |
|---|---|---|
| Statements | 100%(13/13) | 22.22%(12/54) |
| Branches | 100%(0/0) | 0%(0/8) |
| Functions | 100%(1/1) | 16.66%(1/6) |
| Lines | 100%(9/9) | 16%(8/50) |
| 表格檔案數 | 3 | 9 |
沒有任何一行程式碼改變,改的只是「要統計哪些檔案」。
注意 covered 從 13 掉到 12:
collectCoverageFrom寫的是src/**/*.ts,app.html不是.ts就被排除了。
分子分母同時在動,比較兩份報表要看covered/total,不要只看百分比。
| 排除 | 理由 | 決定 |
|---|---|---|
*.spec.ts |
測試檔本身不該計入 | ✅ |
*.d.ts |
純型別宣告,沒有執行期程式碼 | ✅ |
test-setup.ts |
測試框架前置,不是產品程式碼 | ✅ |
main.ts / server.ts |
「不好測」不是理由。server.ts 有 66 行真邏輯 |
❌ 保留 |
留著它們的代價只是門檻數字大一點,但它們是真缺口,排掉就永遠看不到。Ratchet 讓你今天照樣通過,沒必要為了過關先動排除清單。
covered/total 換算| 指標 | covered / total | 未覆蓋 | 門檻 |
|---|---|---|---|
| statements | 12 / 54 | 42 | -42 |
| branches | 0 / 8 | 8 | -8 |
| functions | 1 / 6 | 5 | -5 |
| lines | 8 / 50 | 42 | -42 |
規則:
| 這個指標目前狀態 | 寫法 | 語意 |
|---|---|---|
| 已經 100% | 正數 100 |
只能維持,不能倒退 |
| 還有未覆蓋 | 負數的未覆蓋數量 | 今天幾個沒測到就是幾個,不准再多 |
Ratchet(棘輪):只能收緊、不能放寬,而且從今天就生效。
| 門檻寫法 | 實測 exit | threshold 訊息 | 結論 |
|---|---|---|---|
-0 |
0 | 0 行 | ❌ 等於沒設(見 §10.4) |
0 |
0 | 0 行 | ❌ 永遠不觸發 |
100 |
1 | threshold for statements (100%) not met: 22.22% |
❌ 補齊前每次紅燈 |
-42 / -8 / -5 / -42 |
0 | 0 行 | ✅ 今天就能過 |
push 之後 gitea webhook 自動觸發 d15-happy-path(Pipeline):
$ oc get pipelinerun d14-run-dnxjq -n ci \
-o custom-columns=NAME:.metadata.name,PIPELINE:.spec.pipelineRef.name,MSG:.status.conditions[0].message
NAME PIPELINE MSG
d14-run-dnxjq d15-happy-path Tasks Completed: 8 (Failed: 0, Cancelled 0), Skipped: 0
$ oc get taskrun -n ci -l tekton.dev/pipelineRun=d14-run-dnxjq
NAME SUCCEEDED REASON STARTTIME COMPLETIONTIME
d14-run-dnxjq-git-clone True Succeeded 8m15s 7m49s
d14-run-dnxjq-workspace-probe True Succeeded 7m49s 7m3s
d14-run-dnxjq-gitleaks-scan True Succeeded 7m49s 7m42s
d14-run-dnxjq-semgrep-scan True Succeeded 7m49s 7m33s
d14-run-dnxjq-sca-scan True Succeeded 7m48s 7m37s
d14-run-dnxjq-npm-build True Succeeded 7m3s 46s
d14-run-dnxjq-eslint-check True Succeeded 46s 13s
d14-run-dnxjq-unit-test True Succeeded 46s 7s
unit-test 的 Pod log:
> nx run web:test --coverage --coverageReporters=text --passWithNoTests=false
PASS web apps/web/src/app/format-utils.spec.ts
PASS web apps/web/src/app/app.spec.ts
-----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------|---------|----------|---------|---------|-------------------
All files | 27.58 | 20 | 28.57 | 22.22 |
src | 0 | 0 | 0 | 0 |
main.server.ts | 0 | 100 | 0 | 0 | 1-8
main.ts | 0 | 100 | 0 | 0 | 1-6
server.ts | 0 | 0 | 0 | 0 | 1-66
src/app | 53.33 | 100 | 100 | 46.15 |
app.config.server.ts | 0 | 100 | 100 | 0 | 1-12
app.config.ts | 0 | 100 | 100 | 0 | 1-6
app.routes.server.ts | 0 | 100 | 100 | 0 | 1-3
app.routes.ts | 0 | 100 | 100 | 0 | 3
app.ts | 100 | 100 | 100 | 100 |
format-utils.ts | 100 | 100 | 100 | 100 |
nx-welcome.ts | 100 | 100 | 100 | 100 |
-----------------------|---------|----------|---------|---------|-------------------
Test Suites: 2 passed, 2 total
Tests: 3 passed, 3 total
NX Successfully ran target test for project web
快樂路徑到此結束。 接下來是這道關卡真正的價值:它紅燈的時候。
有人 push 了一個沒有測試的檔案:
// apps/web/src/app/format-utils.ts
export function formatDisplayName(name: string): string {
if (!name) {
return 'Unknown';
}
return name.trim().toUpperCase();
}
5 行、1 個 function、1 個 branch。webhook 觸發的 d15-happy-path(Pipeline):
$ oc get pipelinerun d14-run-rdvn4 -n ci
NAME PIPELINE MSG
d14-run-rdvn4 d15-happy-path Tasks Completed: 8 (Failed: 1, Cancelled 0), Skipped: 0
$ oc get taskrun -n ci -l tekton.dev/pipelineRun=d14-run-rdvn4
NAME SUCCEEDED REASON STARTTIME COMPLETIONTIME
d14-run-rdvn4-git-clone True Succeeded 3m38s 3m13s
d14-run-rdvn4-workspace-probe True Succeeded 3m13s 2m26s
d14-run-rdvn4-gitleaks-scan True Succeeded 3m13s 3m6s
d14-run-rdvn4-semgrep-scan True Succeeded 3m12s 2m57s
d14-run-rdvn4-sca-scan True Succeeded 3m12s 3m1s
d14-run-rdvn4-npm-build True Succeeded 2m26s 53s
d14-run-rdvn4-eslint-check True Succeeded 53s 25s
d14-run-rdvn4-unit-test False StepFailed 53s 21s
第一步永遠是看 TaskRun 清單,不是看 PipelineRun 的紅字。 這裡一眼就知道:eslint-check 綠、只有 unit-test 紅。
PASS web apps/web/src/app/app.spec.ts
App
✓ should render title (220 ms)
-----------------------|---------|----------|---------|---------|-------------------
File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-----------------------|---------|----------|---------|---------|-------------------
All files | 20.68 | 0 | 14.28 | 14.81 |
src/app | 40 | 0 | 50 | 30.76 |
app.ts | 100 | 100 | 100 | 100 |
format-utils.ts | 0 | 0 | 0 | 0 | 1-5 ← 兇手
nx-welcome.ts | 100 | 100 | 100 | 100 |
-----------------------|---------|----------|---------|---------|-------------------
Jest: Uncovered count for statements (46) exceeds global threshold (42)
Jest: Uncovered count for branches (10) exceeds global threshold (8)
Jest: Uncovered count for lines (46) exceeds global threshold (42)
Jest: Uncovered count for functions (6) exceeds global threshold (5)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 8.628 s
Ran all test suites.
NX Running target test for project web failed
| 位置 | 意思 |
|---|---|
Tests: 1 passed, 1 total |
測試全過。 紅燈完全不是測試壞掉,是覆蓋率退步 |
Uncovered count for statements (46) exceeds global threshold (42) |
講的是數量不是百分比 —— 這正是負數門檻的語意。超過 4 個 |
format-utils.ts | 0 | 0 | 0 | 0 | 1-5 |
兇手直接列在表上,還標了未覆蓋行號 |
Cache: Skipped (--skip-nx-cache) |
這條 log 是這次真跑出來的,不是重播的 |
| 指標 | 門檻 | 現在 | 差 |
|---|---|---|---|
| statements | 42 | 46 | +4 |
| branches | 8 | 10 | +2 |
| functions | 5 | 6 | +1 |
| lines | 42 | 46 | +4 |
那 5 行程式碼帶進來 4 個 statement、2 個 branch、1 個 function、4 行。通通要覆蓋掉。
// apps/web/src/app/format-utils.spec.ts
import { formatDisplayName } from './format-utils';
describe('formatDisplayName', () => {
it('大寫化並去掉前後空白', () => {
expect(formatDisplayName(' ada lovelace ')).toBe('ADA LOVELACE');
});
// 這一條不是湊數:少了它,if (!name) 那個分支不會被走到,
// branches 未覆蓋數會是 9 > 8,unit-test 還是紅燈。
it('空字串回傳 Unknown', () => {
expect(formatDisplayName('')).toBe('Unknown');
});
});
如果只寫第一條測試:
if (!name) 的 true 分支沒走到 ❌ → branches 未覆蓋 9 > 8
測試有寫、也通過了,Task 還是紅燈。
這是整篇最容易踩到的地方。看到 branches 那一行沒降下來,就是分支沒走完,不是測試沒寫。
補上完整的 spec 之後(d14-run-dnxjq,見 §五),四項未覆蓋數量:
| 指標 | 總數 | 已覆蓋 | 未覆蓋 | 門檻 | 餘裕 |
|---|---|---|---|---|---|
| statements | 58 | 16 | 42 | 42 | 0 |
| branches | 10 | 2 | 8 | 8 | 0 |
| functions | 7 | 2 | 5 | 5 | 0 |
| lines | 54 | 12 | 42 | 42 | 0 |
四項全部剛好卡在門檻上,一個都不能少。
這正是 Ratchet 的設計意圖:新增的程式碼必須自己把自己覆蓋掉,不能吃既有的餘裕。
紅(d14-run-rdvn4) |
綠(d14-run-dnxjq) |
|
|---|---|---|
| Statements | 20.68% | 27.58% |
| Branches | 0% | 20% |
| Functions | 14.28% | 28.57% |
| Lines | 14.81% | 22.22% |
format-utils.ts |
0 / 0 / 0 / 0 | 100 / 100 / 100 / 100 |
| Test Suites | 1 passed | 2 passed |
| Tests | 1 passed | 3 passed |
unit-test |
StepFailed | Succeeded |
| PipelineRun | Failed(8 完成 / 1 失敗) | Succeeded(8 完成 / 0 失敗) |
兩筆之間只加了一個檔案。 門檻是雙向可靠的:踩線會擋,補上會放行。
同一批 commit 裡還有一筆 d14-run-6hqvx:
| PipelineRun | eslint-check | unit-test |
|---|---|---|
d14-run-6hqvx |
StepFailed | StepFailed |
d14-run-rdvn4 |
Succeeded | StepFailed |
d14-run-dnxjq |
Succeeded | Succeeded |
第一筆的 eslint-check 是被一個手誤擋的:jest.config.cts 裡 '\\.' 少打一個反斜線變成 '\.'。
jest 完全無感(正則裡未逃脫的 . 是「任意字元」,一樣會匹配),eslint 的 no-useless-escape 有感。
於是兩道關卡同時紅,只看 PipelineRun 的 Failed 完全分不出來。
教訓:
unit-test跟eslint-check並行的代價,就是紅燈時要多看一步 TaskRun 清單。這個代價值得付 —— 換來的是一次 run 就知道兩件事。
nx test web --coverage
→ exit 0,測試 PASS,Pod log 上沒有任何覆蓋率表格
第一次撞到會以為「旗標被 Nx 吃掉了」。不是。 進容器看:
coverage/apps/web/index.html
coverage/apps/web/base.css
coverage/apps/web/prettify.js
jest --showConfig 給出答案:
coverageReporters = ["html"]
@nx/jest 的 preset 把它覆寫成 ['html']。覆蓋率量了、報表產了、寫進容器了,然後隨 Pod 一起消失。
這比「旗標被吃掉」難發現得多:行為不是「沒量」,是「量了但你看不到」。而且如果設了 coverageThreshold,門檻是真的在檢查 —— 只是通過或失敗的理由你在 log 上讀不到。
修法:--coverageReporters=text,或設定檔寫 coverageReporters: ['text', 'json-summary', 'html']。
整個 repo grep passWithNoTests:一次都沒出現。但:
$ mv apps/web/src/app/app.spec.ts /tmp/
$ nx test web
No tests found, exiting with code 0
→ exit 0,NX Successfully ran target test for project web
放水的是 @nx/jest:jest executor 的預設值,設定檔上完全看不到。
$ nx test web --passWithNoTests=false
No tests found, exiting with code 1
Run with `--passWithNoTests` to exit with code 0
→ exit 1
危險的不是「有人加了一個放水旗標」,是「預設就放水,而且 code review 時看不到」。testMatch 寫錯、測試目錄被誤刪、新專案還沒寫測試 —— 在 CI 上全部是綠燈,diff 裡沒有任何線索。
這道關卡的邊界:擋得住「一個 spec 都沒有」,擋不住「有 1 個 spec 只涵蓋一小角」。門檻從「零」提高到「一」。剩下的交給
coverageThreshold。
-0 這個門檻是空的想表達「未覆蓋數量上限是零」而寫 -0:
threshold < 0
-0 < 0 是 false
actual < -0 → 永遠通過
實測佐證:-0 那一輪 exit 0,而且 log 裡 threshold 相關訊息是 0 行 —— 它連檢查都沒去做。
已達標的指標用正數 100。
nx 有 task cache。同一組輸入的 test target 跑過一次之後,下一次可能直接重播上次的結果。
被快取重播的綠燈,在 log 上跟真跑出來的綠燈長得幾乎一樣。 所以 Task 裡加了 --skip-nx-cache,log 會明確寫:
Cache: Skipped (--skip-nx-cache)
每個 PipelineRun 都是新 clone,快取本來就是冷的,成本接近零。
eslint-check目前沒有加,這是既有的不一致,不是本篇引入的。
加一個完全沒有 expect 的測試:
import * as routes from './app.routes';
it('touches the code', () => { void routes; });
實測:
| 指標 | 加之前 | 加之後 |
|---|---|---|
| Statements | 22.22%(12/54) | 24.07%(13/54) |
| Lines | 16%(8/50) | 18%(9/50) |
app.routes.ts |
0% | 100% |
測試通過,覆蓋率上升,app.routes.ts 從 0% 變 100% —— 而這個測試沒有驗證任何事情。
覆蓋率門檻擋的是「這段程式碼有沒有被執行過」,不是「它有沒有被驗證過」。
跟 Day 21〈Trivy fs〉「0 個漏洞不代表掃描有效」是同一種問題的另一個面貌。
所以 coverageThreshold 不是品質保證,是退步偵測。 它能告訴你「今天比昨天差」,不能告訴你「今天夠好」。當成前者用是對的,當成後者用會出事。
NODE_OPTIONS 的數字要自己量| 情境 | 峰值 RSS(全容器,1 秒取樣) |
|---|---|
不設 collectCoverageFrom |
517 MB / 689 MB |
設了 collectCoverageFrom |
1441 MB / 1722 MB |
| Node 在這台的預設 heap 上限 | 2096 MB |
補上統計範圍讓記憶體漲了約 2.5 倍 —— instrument 的檔案從 3 個變 9 個,其中 nx-welcome.ts 一個檔就 809 行。
1722 MB 對 2096 MB,只剩約 18% 餘裕,而這還只有 1 個專案 9 個檔案。所以設:
- name: NODE_OPTIONS
value: --max-old-space-size=3072
3072 是從 1722 推的(約 1.8 倍),設定後實測 heap 上限變成 3120 MB。
量之前我以為「一個專案應該不用設」,結果
collectCoverageFrom一加就逼到 82%。
兩個方向的直覺都是錯的,只有量出來的數字是對的。
Allocatable: memory 11781652Ki(約 11.2 GiB)
Allocated: memory requests 10181Mi (88%)
記憶體 requests 已在 88%,餘裕約 1.5 GiB。 幫 unit-test 加 memory request 會很接近排不進去。目前不設 request/limit 反而是它跑得起來的原因。換到有餘裕的叢集要設;在這台,先確認排得進去再設。
| 現象 | 說明 |
|---|---|
git-clone 報 cannot copy ... pre-push.sample: File exists |
固定 PVC pipeline-source-pvc 有上一輪殘留。手動建 PipelineRun 時改用 volumeClaimTemplate |
push 之後自動多出 d14-run-* |
gitea webhook → el-d14-gitea-ci EventListener,它的 pipelineRef 就是 d15-happy-path(Pipeline) |
多條 pipeline 同時跑時 oc 斷線 |
這台記憶體 requests 已在 88%,偶發 wsarecv: connection forcibly closed,重試即可 |
grep --include="*.ts" 掃不到設定檔 |
檔名是 jest.config.cts。要用 --include="*.[cm]?ts" 或直接 find |
接關卡時
unit-test 接在 npm-build 之後,跟 eslint-check 並行 —— 它要 node_modules
test -d node_modules,runAfter 被拿掉時會明確報錯unit-test 裡自己 npm ci —— 那是 31 秒 / 774.9 MB 的純浪費設定覆蓋率時
collectCoverageFrom —— 統計範圍沒修之前,門檻設多少都沒意義coverageReporters 含 text —— Nx preset 預設 ['html']
*.d.ts、*.spec.ts 可以;「這個檔不好測」不可以covered/total 換算;已達標用正數 100;不要用 -0
--passWithNoTests=false —— 不要假設 repo 裡沒這字串就等於沒在放水--skip-nx-cache —— 否則不知道綠燈是跑出來的還是重播的NODE_OPTIONS 的數字自己量。這台量到 1722 MB / 上限 2096 MB,所以設 3072紅燈時
Tests: N passed —— 測試全過的話,擋的是覆蓋率不是測試Uncovered count (X) exceeds (Y) —— 差幾個一目瞭然Uncovered Line #s
第一部做了三件事:
unit-test 接進 DAG,跟 eslint-check 並行,整條線只多約 20 秒jest.config.cts 補上報表出口、統計範圍、門檻三段covered/total 換算,做到「今天就能過」跟「以後不能變差」第二部的重點只有一句:
Tests: 1 passed, 1 total跟unit-test StepFailed可以同時成立。
測試全過、Task 紅燈,這不是矛盾 —— 這道關卡量的本來就不是「測試有沒有壞」,是「覆蓋率有沒有退步」。看懂這件事,紅燈就從「哪裡壞了」變成「哪裡少測了」。
而修它的時候要記得:補了測試不等於補夠了測試。四項指標裡最容易漏的是 branches,因為「函式呼叫過了」跟「每條路都走過了」是兩回事。
這一路上三個不會報錯的陷阱 —— 統計範圍是空的、報表出口是 HTML、放水是預設值 —— 共同結構都一樣:CI 綠燈、log 正常、設定檔乾淨,而防線根本沒在防。
光看設定檔推論不夠,光看現象推論也不夠。要進去看它產出了什麼檔案,要實際踩一次線看它會不會叫。
明天(Day 23),我們將進入型別安全防線,探討多重 tsconfig 檢查的必要性,並實作 SBOM 產出時的除錯與解析作業。
以下連結多半指向各專案的 latest 文件,跟本文實測的 Jest 30.3 / Nx 23.1.1 / CRC 4.21.14 不保證一致。換版本時以實測為準,文件只作為交叉查核用。
| 文件 | 對應本文 |
|---|---|
Configuring Jest — collectCoverageFrom、coverageThreshold、coverageReporters 的官方定義 |
§3、§4 |
Jest CLI Options — --coverage、--coverageReporters、--passWithNoTests、--showConfig |
§2.1、§10.2、§10.3 |
CoverageReporter.ts(Jest 原始碼) — 負數門檻的判斷就在這裡:if (threshold < 0),以及 Uncovered count for ... exceeds ... threshold 這行訊息的來源 |
§4.2、§7.1、§10.4 |
| 文件 | 對應本文 |
|---|---|
| @nx/jest Executors — executor 選項總覽 | §10.3 |
@nx/jest:jest schema.json(Nx 原始碼) — executor 選項的權威來源,codeCoverage、passWithNoTests 的預設值要在這裡看,不是在 jest.config |
§10.3 |
Skip Task Caching — --skip-nx-cache |
§2.1、§10.5 |
| How caching works — 快取命中時「終端輸出會被原樣重播」的機制說明 | §10.5 |
| 文件 | 對應本文 |
|---|---|
Pipelines — runAfter 與 DAG 的並行行為 |
§1 |
| Tasks — step、workspace、script | §2 |
PipelineRuns — status 與 conditions 欄位,oc get pipelinerun 讀的就是這些 |
§5、§6 |
| Compute Resources in Tekton — Step 的 request/limit 如何被加總、LimitRange 怎麼介入 | §12.1 |
| EventListeners — webhook 進來之後那顆 pod 是怎麼來的 | §13 |
| 文件 | 對應本文 |
|---|---|
Node.js Command-line API — NODE_OPTIONS、--max-old-space-size |
§12 |
相等比較與相同性(MDN 繁中) — -0 與 +0 在 ==/=== 下被視為相同 |
§10.4 |
Object.is()(MDN) — 唯一能分辨 -0 的比較方式,反過來證明 -0 < 0 為什麼是 false |
§10.4 |
| 文件 | 對應本文 |
|---|---|
ESLint no-useless-escape — 那個「jest 無感、eslint 有感」的手誤,規則本體在這裡 |
§10.1 |
| Gitea Webhooks — push 之後自動多出 PipelineRun 的起點 | §13 |